{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Electrical Power Generation 1\n",
    "\n",
    "## Objective and Prerequisites\n",
    "\n",
    "Major electric power companies around the world utilize mathematical optimization to manage the flow of energy across their electrical grids. In this example, you’ll discover the power of mathematical optimization in addressing a common energy industry problem: electrical power generation. We’ll show you how to figure out the optimal set of power stations to turn on in order to satisfy anticipated power demand over a 24-hour time horizon.\n",
    "\n",
    "This model is example 15 from the fifth edition of Model Building in Mathematical Programming by H. Paul Williams on pages 270 – 271 and 325 – 326.\n",
    "\n",
    "This example is at the intermediate level, where we assume that you know Python and the Gurobi Python API and that you have some knowledge of building mathematical optimization models.\n",
    "\n",
    "**Download the Repository** <br />\n",
    "You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip). \n",
    "\n",
    "---\n",
    "## Problem Description\n",
    "\n",
    "In this problem, power generation units are grouped into three distinct types, with different characteristics for each type (power output, cost per megawatt hour, startup cost, etc.).  A unit can be on or off, with a startup cost associated with transitioning from off to on, and power output that can fall anywhere between a specified minimum and maximum value when the unit is on.  A 24-hour time horizon is divided into 5 discrete time periods, each with an expected total power demand.  The model decides which units to turn on, and when, in order to satisfy demand for each time period.  The model also captures a reserve requirement, where the selected power plants must be capable of increasing their output, while still respecting their maximum output, in order to cope with the situation where actual demand exceeds predicted demand.\n",
    "\n",
    "A set of generators is available to satisfy power demand for the following day.  Anticipated demand is as follows:\n",
    "\n",
    "| Time Period | Demand (megawatts) |\n",
    "| --- | --- |\n",
    "| 12 pm to 6 am | 15000 |\n",
    "| 6 am to 9 am | 30000 |\n",
    "| 9 am to 3 pm | 25000 |\n",
    "| 3 pm to 6 pm | 40000 |\n",
    "| 6 pm to 12 pm | 27000 |\n",
    "\n",
    "Generators are grouped into three types, with the following minimum and maximum output for each type (when they are on):\n",
    "\n",
    "| Type | Number available | Minimum output (MW) | Maximum output (MW) |\n",
    "| --- | --- | --- | --- |\n",
    "| 0 | 12 |  850 | 2000 |\n",
    "| 1 | 10 | 1250 | 1750 |\n",
    "| 2 | 5 | 1500 | 4000 |\n",
    "\n",
    "There are costs associated with using a generator: a cost per hour when the generator is on (and generating its minimum output), a cost per megawatt hour above its minimum, and a startup cost for turning a generator on:\n",
    "\n",
    "| Type | Cost per hour (when on) | Cost per MWh above minimum | Startup cost |\n",
    "| --- | --- | --- | --- |\n",
    "| 0 | $\\$1000$ | $\\$2.00$ | $\\$2000$ |\n",
    "| 1 | $\\$2600$ | $\\$1.30$ | $\\$1000$ |\n",
    "| 2 | $\\$3000$ | $\\$3.00$ | $\\$500$ |\n",
    "\n",
    "Generators must meet predicted demand, but they must also have sufficient reserve capacity to be able to cope with the situation where actual demand exceeds predicted demand.  For this model, the set of selected generators must be able to produce as much as 115% of predicted demand.\n",
    "\n",
    "Which generators should be committed to meeting anticipated demand in order to minimize total cost?\n",
    "\n",
    "---\n",
    "## Model Formulation\n",
    "\n",
    "### Sets and Indices\n",
    "\n",
    "$t \\in \\text{Types}=\\{0,1,2\\}$: Types of generators.\n",
    "\n",
    "$p \\in \\text{Periods}=\\{0,1,2,3,4\\}$: Time periods.\n",
    "\n",
    "### Parameters\n",
    "\n",
    "$\\text{period_hours}_p \\in \\mathbb{N}^+$: Number of hours in each time period.\n",
    "\n",
    "$\\text{generators}_t \\in \\mathbb{N}^+$: Number of generators of type $t$.\n",
    "\n",
    "$\\text{demand}_p \\in \\mathbb{R}^+$: Total power demand for time period $p$.\n",
    "\n",
    "$\\text{start0} \\in \\mathbb{N}^+$: Number of generators that are on at the beginning of the time horizon (and available in time period 0 without paying a startup cost).\n",
    "\n",
    "$\\text{min_output}_t \\in \\mathbb{R}^+$: Minimum output for generator type $t$ (when on).\n",
    "\n",
    "$\\text{max_output}_t \\in \\mathbb{R}^+$: Maximum output for generator type $t$.\n",
    "\n",
    "$\\text{base_cost}_t \\in \\mathbb{R}^+$: Minimum operating cost (per hour) for a generator of type $t$.\n",
    "\n",
    "$\\text{per_mw_cost}_t \\in \\mathbb{R}^+$: Cost to generate one additional MW (per hour) for a generator of type $t$.\n",
    "\n",
    "$\\text{startup_cost}_t \\in \\mathbb{R}^+$: Startup cost for generator of type $t$.\n",
    "\n",
    "### Decision Variables\n",
    "\n",
    "$\\text{ngen}_{t,p} \\in \\mathbb{N}^+$: Number of generators of type $t$ that are on in time period $p$.\n",
    "\n",
    "$\\text{output}_{t,p} \\in \\mathbb{R}^+$: Total power output from generators of type $t$ in time period $p$.\n",
    "\n",
    "$\\text{nstart}_{t,p} \\in \\mathbb{N}^+$: Number of generators of type $t$ to start in time period $p$.\n",
    "\n",
    "\n",
    "\n",
    "### Objective Function\n",
    "\n",
    "- **Cost**: Minimize the cost (in USD) to satisfy the predicted electricity demand.\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{Minimize} \\quad Z_{on} + Z_{extra} + Z_{startup}\n",
    "\\end{equation}\n",
    "\n",
    "\\begin{equation}\n",
    "Z_{on} = \\sum_{(t,p) \\in \\text{Types} \\times \\text{Periods}}{\\text{base_cost}_t*\\text{ngen}_{t,p}}\n",
    "\\end{equation}\n",
    "\\begin{equation}\n",
    "Z_{extra} = \\sum_{(t,p) \\in \\text{Types} \\times \\text{Periods}}{\\text{per_mw_cost}_t*(\\text{output}_{t,p} - \\text{min_load}_t})\n",
    "\\end{equation}\n",
    "\\begin{equation}\n",
    "Z_{startup} = \\sum_{(t,p) \\in \\text{Types} \\times \\text{Periods}}{\\text{startup_cost}_t*\\text{nstart}_{t,p}}\n",
    "\\end{equation}\n",
    "\n",
    "\n",
    "### Constraints\n",
    "\n",
    "- **Available generators**: Number of generators used must be less than or equal to the number available.\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{ngen}_{t,p} \\leq \\text{generators}_{t} \\quad \\forall (t,p) \\in \\text{Types} \\times \\text{Periods}\n",
    "\\end{equation}\n",
    "\n",
    "- **Demand**: Total power generated across all generator types must meet anticipated demand for each time period $p$.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{t \\in \\text{Types}}{\\text{output}_{t,p}} \\geq \\text{demand}_p \\quad \\forall p \\in \\text{Periods}\n",
    "\\end{equation}\n",
    "\n",
    "- **Min/max generation**: Power generation must respect generator min/max values.\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{output}_{t,p} \\geq \\text{min_output}_t*\\text{ngen}_{t,p} \\quad \\forall (t,p) \\in \\text{Types} \\times \\text{Periods}\n",
    "\\end{equation}\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{output}_{t,p} \\leq \\text{max_output}_t*\\text{ngen}_{t,p} \\quad \\forall (t,p) \\in \\text{Types} \\times \\text{Periods}\n",
    "\\end{equation}\n",
    "\n",
    "- **Reserve**: Selected generators must be able to satisfy demand that is as much as 15% above the prediction.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{t \\in \\text{Types}}{\\text{max_output}_t*\\text{ngen}_{t,p}} \\geq 1.15 * \\text{demand}_p \\quad \\forall p \\in \\text{Periods}\n",
    "\\end{equation}\n",
    "\n",
    "- **Startup**: Establish relationship between number of active generators and number of startups (use $start0$ for period before the time horizon starts)\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{ngen}_{t,p} \\leq \\text{ngen}_{t,p-1} + \\text{startup}_{t,p} \\quad \\forall (t,p) \\in \\text{Types} \\times \\text{Periods}\n",
    "\\end{equation}\n",
    "\n",
    "---\n",
    "## Python Implementation\n",
    "\n",
    "We import the Gurobi Python Module and other Python libraries."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install gurobipy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "# tested with Python 3.11 & Gurobi 11.0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Input Data\n",
    "We define all the input data of the model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Parameters\n",
    "\n",
    "ntypes = 3\n",
    "nperiods = 5\n",
    "maxstart0 = 5\n",
    "\n",
    "generators = [12, 10, 5]\n",
    "period_hours = [6, 3, 6, 3, 6]\n",
    "demand = [15000, 30000, 25000, 40000, 27000]\n",
    "min_load = [850, 1250, 1500]\n",
    "max_load = [2000, 1750, 4000]\n",
    "base_cost = [1000, 2600, 3000]\n",
    "per_mw_cost = [2, 1.3, 3]\n",
    "startup_cost = [2000, 1000, 500]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Model Deployment\n",
    "\n",
    "We create a model and the variables. For each time period, we have: an integer decision variable to capture the number of active generators of each type (ngen), an integer variable to capture the number of generators of each type we must start (nstart), and a continuous decision variable to capture the total power output for each generator type (output)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Using license file c:\\gurobi\\gurobi.lic\n"
     ]
    }
   ],
   "source": [
    "model = gp.Model('PowerGeneration')\n",
    "ngen = model.addVars(ntypes, nperiods, vtype=GRB.INTEGER, name=\"ngen\")\n",
    "nstart = model.addVars(ntypes, nperiods, vtype=GRB.INTEGER, name=\"nstart\")\n",
    "output = model.addVars(ntypes, nperiods, vtype=GRB.CONTINUOUS, name=\"genoutput\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next we insert the constraints:\n",
    "\n",
    "The number of active generators can't exceed the number of generators."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Generator count\n",
    "\n",
    "numgen = model.addConstrs(ngen[type, period] <= generators[type]\n",
    "                         for type in range(ntypes) for period in range(nperiods))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Total power output for a generator type depends on the number of generators of that type that are active."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Respect minimum and maximum output per generator type\n",
    "\n",
    "min_output = model.addConstrs((output[type, period] >= min_load[type] * ngen[type, period])\n",
    "                              for type in range(ntypes) for period in range(nperiods))\n",
    "\n",
    "max_output = model.addConstrs((output[type, period] <= max_load[type] * ngen[type, period])\n",
    "                              for type in range(ntypes) for period in range(nperiods))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Total output for each time period must meet predicted demand."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Meet demand\n",
    "\n",
    "meet_demand = model.addConstrs(gp.quicksum(output[type, period] for type in range(ntypes)) >= demand[period]\n",
    "                               for period in range(nperiods))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Selected generators must be able to cope with an excess of demand."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Provide sufficient reserve capacity\n",
    "\n",
    "reserve = model.addConstrs(gp.quicksum(max_load[type]*ngen[type, period] for type in range(ntypes)) >= 1.15*demand[period]\n",
    "                    for period in range(nperiods))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Connect the decision variables that capture active generators with the decision variables that count startups."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Startup constraint\n",
    "\n",
    "startup0 = model.addConstrs((ngen[type,0] <= maxstart0 + nstart[type,0])\n",
    "                            for type in range(ntypes))\n",
    "\n",
    "startup = model.addConstrs((ngen[type,period] <= ngen[type,period-1] + nstart[type,period])\n",
    "                           for type in range(ntypes) for period in range(1,nperiods))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Objective: minimize total cost.  Cost consists of three components: the cost for running active generation units, the cost to generate power beyond the minimum for each unit, and the cost to start up generation units."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Objective: minimize total cost\n",
    "\n",
    "active = gp.quicksum(base_cost[type]*period_hours[period]*ngen[type,period]\n",
    "                    for type in range(ntypes) for period in range(nperiods))\n",
    "\n",
    "per_mw = gp.quicksum(per_mw_cost[type]*period_hours[period]*(output[type,period] - min_load[type]*ngen[type,period])\n",
    "                       for type in range(ntypes) for period in range(nperiods))\n",
    "\n",
    "startup_obj = gp.quicksum(startup_cost[type]*nstart[type,period]\n",
    "                         for type in range(ntypes) for period in range(nperiods))\n",
    "\n",
    "model.setObjective(active + per_mw + startup_obj)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, we start the optimization and Gurobi finds the optimal solution."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (win64)\n",
      "Thread count: 4 physical cores, 8 logical processors, using up to 8 threads\n",
      "Optimize a model with 70 rows, 45 columns and 147 nonzeros\n",
      "Model fingerprint: 0x08cf02b0\n",
      "Variable types: 15 continuous, 30 integer (0 binary)\n",
      "Coefficient statistics:\n",
      "  Matrix range     [1e+00, 4e+03]\n",
      "  Objective range  [4e+00, 9e+03]\n",
      "  Bounds range     [0e+00, 0e+00]\n",
      "  RHS range        [5e+00, 5e+04]\n",
      "Presolve removed 16 rows and 1 columns\n",
      "Presolve time: 0.00s\n",
      "Presolved: 54 rows, 44 columns, 130 nonzeros\n",
      "Variable types: 0 continuous, 44 integer (0 binary)\n",
      "Found heuristic solution: objective 1123913.3000\n",
      "Found heuristic solution: objective 1084300.0000\n",
      "\n",
      "Root relaxation: objective 9.995143e+05, 23 iterations, 0.00 seconds\n",
      "\n",
      "    Nodes    |    Current Node    |     Objective Bounds      |     Work\n",
      " Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time\n",
      "\n",
      "     0     0 999514.286    0    7 1084300.00 999514.286  7.82%     -    0s\n",
      "H    0     0                    1009430.8000 999514.286  0.98%     -    0s\n",
      "H    0     0                    1006720.0000 999514.286  0.72%     -    0s\n",
      "H    0     0                    1005970.0000 999514.286  0.64%     -    0s\n",
      "H    0     0                    1003290.0000 999514.286  0.38%     -    0s\n",
      "H    0     0                    1002540.0000 999514.286  0.30%     -    0s\n",
      "\n",
      "Cutting planes:\n",
      "  MIR: 4\n",
      "  StrongCG: 1\n",
      "\n",
      "Explored 1 nodes (23 simplex iterations) in 0.06 seconds\n",
      "Thread count was 8 (of 8 available processors)\n",
      "\n",
      "Solution count 7: 1.00254e+06 1.00329e+06 1.00597e+06 ... 1.12391e+06\n",
      "\n",
      "Optimal solution found (tolerance 1.00e-04)\n",
      "Best objective 1.002540000000e+06, best bound 1.002540000000e+06, gap 0.0000%\n"
     ]
    }
   ],
   "source": [
    "model.write('junk.lp')\n",
    "model.optimize()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## Analysis\n",
    "\n",
    "The anticipated demand for electricity over the 24-hour time window can be met for a total cost of $\\$1,002,540$. The detailed plan for each time period is as follows.\n",
    "\n",
    "### Unit Commitments\n",
    "\n",
    "The following table shows the number of generators of each type that are active in each time period in the optimal solution:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>0</th>\n",
       "      <th>1</th>\n",
       "      <th>2</th>\n",
       "      <th>3</th>\n",
       "      <th>4</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Type0</th>\n",
       "      <td>12.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>12.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Type1</th>\n",
       "      <td>3.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>9.0</td>\n",
       "      <td>9.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Type2</th>\n",
       "      <td>-0.0</td>\n",
       "      <td>-0.0</td>\n",
       "      <td>-0.0</td>\n",
       "      <td>2.0</td>\n",
       "      <td>-0.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          0     1     2     3     4\n",
       "Type0  12.0  12.0  12.0  12.0  12.0\n",
       "Type1   3.0   8.0   8.0   9.0   9.0\n",
       "Type2  -0.0  -0.0  -0.0   2.0  -0.0"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "rows = [\"Type\" + str(t) for t in range(ntypes)]\n",
    "units = pd.DataFrame(columns=range(nperiods), index=rows, data=0.0)\n",
    "\n",
    "for t in range(ntypes):\n",
    "    for p in range(nperiods):\n",
    "        units.loc[\"Type\"+str(t), p] = ngen[t,p].x\n",
    "units"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The following shows the number of generators of each type that must be started in each time period to achieve this plan (recall that the model assumes that 5 are available at the beginning of the time horizon):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>0</th>\n",
       "      <th>1</th>\n",
       "      <th>2</th>\n",
       "      <th>3</th>\n",
       "      <th>4</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Type0</th>\n",
       "      <td>7.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Type1</th>\n",
       "      <td>0.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>Type2</th>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>2.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         0    1    2    3    4\n",
       "Type0  7.0  0.0  0.0  0.0  0.0\n",
       "Type1  0.0  5.0  0.0  1.0  0.0\n",
       "Type2  0.0  0.0  0.0  2.0  0.0"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "startups = pd.DataFrame(columns=range(nperiods), index=rows, data=0.0)\n",
    "\n",
    "for t in range(ntypes):\n",
    "    for p in range(nperiods):\n",
    "        startups.loc[\"Type\"+str(t), p] = int(nstart[t,p].x)\n",
    "startups"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## References\n",
    "\n",
    "H. Paul Williams, Model Building in Mathematical Programming, fifth edition.\n",
    "\n",
    "Copyright © 2020 Gurobi Optimization, LLC"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
